using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Security;
using System.Security.Permissions;
using System.Security.Policy;
using Microsoft.Xna.Framework;
using Xfinity.Entities.Managers;
using Microsoft.Xna.Framework.Content;
using System.CodeDom;
using System.Security.Cryptography;
using System.Text;

namespace Xfinity.Scripting
{
    #region Other stuff
    [Serializable]
    public class ScriptCompileException : Exception
    {

        private CompilerResults compilerResults;
        /// <summary>
        /// Gets the compiler results.
        /// </summary>
        /// <value>The compiler results.</value>
        public CompilerResults CompilerResults
        {
            get { return compilerResults; }
        }

        public ScriptCompileException(string message, CompilerResults compilerResults)
            : base(message)
        {
            this.compilerResults = compilerResults;
        }

    }
    public sealed class HashAttribute : Attribute
    {
        public HashAttribute(string bytes)
        {
            this.bytes = bytes;
        }
        private string bytes;

        /// <summary>
        /// 
        /// </summary>
        public string Bytes
        {
            get { return bytes; }
            set { bytes = value; }
        }

    }
    public sealed class ClrVersionAttribute : Attribute
    {
        private string version;

        /// <summary>
        /// 
        /// </summary>
        public string Version
        {
            get { return version; }
            set { version = value; }
        }
        public ClrVersionAttribute()
        {
            version = Environment.Version.ToString();
        }
        public ClrVersionAttribute(string version)
        {
            this.version = version;
        }
    }
    #endregion
    public class ScriptManager : GlobalManager
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="ScriptManager"/> class.
        /// </summary>
        /// <param name="name">The name of the manager.</param>
        /// <param name="game">The Game.</param>
        public ScriptManager(string name, Game game)
            : base(name, false, game)
        {

        }

        private static List<Script> scripts = new List<Script>();

        /// <summary>
        /// Gets the scripts.
        /// </summary>
        /// <value>The scripts.</value>
        [SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists")]
        public static List<Script> Scripts
        {
            [DebuggerStepThrough]
            get { return scripts; }
        }

        PolicyLevel securityLevel;
        private bool initialized = false;
        /// <summary>
        /// Initialises the global manager.
        /// </summary>
        public override void Initialize()
        {
            if (initialized)
            {
                return;
            }
            initialized = true;
            // Create the security policy level for this application domain
            securityLevel = PolicyLevel.CreateAppDomainLevel();

            // Create a new, empty permission set so we don't mistakenly grant some permission we don't want
            PermissionSet permissions = new PermissionSet(PermissionState.None);

            // Set the permissions that you will allow, in this case we only want to allow execution of code...
            permissions.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));

            // ...and to reflect...
            permissions.AddPermission(new ReflectionPermission(ReflectionPermissionFlag.AllFlags));

            // ...and to access isolated storage.
            permissions.AddPermission(new IsolatedStorageFilePermission(PermissionState.Unrestricted));

            // Make sure we have the permissions currently
            permissions.Demand();

            // Give the policy level's root code group a new policy statement based
            // on the new permission set.
            securityLevel.RootCodeGroup.PolicyStatement = new PolicyStatement(permissions);
            
            source = new string[scripts.Count];


            for (int i = 0; i < scripts.Count; i++)
            {
                source[i] = scripts[i];
            }
            if (source.Length == 0)
            {
                source = new string[1];
                source[0] = "";
            }
            CompileScripts();
            base.Initialize();
        }

        /// <summary>
        /// Comiples the scripts.
        /// </summary>
        /// <param name="source">The source.</param>
        public void CompileScripts(string[] source)
        {
            string[] oldSources = this.source;
            this.source = source;
            CompileScripts();
            this.source = oldSources;
        }

        /// <summary>
        /// Compiles the scripts.
        /// </summary>
        public void CompileScripts()
        {
            string generic = File.ReadAllText(Path.GetFullPath(Path.Combine(Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location),Xfinity.Resources.Resources.ScriptsPath), Xfinity.Resources.Resources.GenericScriptFileName)));
            Array.Resize<string>(ref source, source.Length + 1);
            source[source.Length - 1] = generic;
            provider = CodeDomProvider.CreateProvider("CSharp");

            if (File.Exists(Path.GetFullPath(Xfinity.Resources.Resources.CompiledScriptsPath)))
            {
                // need to load this way because of a quirk of the reflection-only context.
                byte[] assemblyBytes = File.ReadAllBytes(Path.GetFullPath(Xfinity.Resources.Resources.CompiledScriptsPath));
                Assembly existing = Assembly.Load(assemblyBytes);

                AssemblyName[] referenced = existing.GetReferencedAssemblies();
                AssemblyName xfinityName = Assembly.GetExecutingAssembly().GetName();
                foreach (AssemblyName name in referenced)
                {
                    // recompile if the scripts were compiled against an old version of the library
                    if (name.Name == xfinityName.Name)
                    {
                        if (name.Version != xfinityName.Version)
                        {
                            Trace.WriteLine("Scripts were built against an old version of the engine; recompiling.");
                            Trace.WriteLine(name.Version.ToString() + " versus " + xfinityName.Version.ToString());
                            Compile();
                            LoadScriptAssembly();
                            return;
                        }
                    }
                }

                byte[] bytes = HashSource();

                HashAttribute[] hashes = ((HashAttribute[])existing.GetCustomAttributes(typeof(HashAttribute), true));
                if (hashes.Length == 0)
                {
                    // recompile if no hash
                    Trace.WriteLine("Compiled scripts have an invalid hash; recompiling.");
                    Compile();
                    LoadScriptAssembly();
                    return;
                }
                ClrVersionAttribute[] versions = ((ClrVersionAttribute[])existing.GetCustomAttributes(typeof(ClrVersionAttribute), true));
                if (versions.Length == 0)
                {
                    // recompile if no version attribute
                    Trace.WriteLine("Compiled scripts missing ClrVersionAttribute; recompiling.");
                    Compile();
                    LoadScriptAssembly();
                    return;
                }

                ClrVersionAttribute version = versions[0];
                if (version.Version != Environment.Version.ToString())
                {
                    // recompile if different .net framework verison
                    Trace.WriteLine("Scripts were compiled for a different version of the CLR; recompiling.");
                    Compile();
                    LoadScriptAssembly();
                    return;
                }



                MD5 sum = MD5.Create();
                HashAttribute h = hashes[0];
                byte[] hash = sum.ComputeHash(bytes);
                string textHash = CreateTextualHash(hash);
                if (h != null && h.Bytes != textHash)
                {
                    Trace.WriteLine("Scripts have changed since last compile; recompiling.");
                    Compile(bytes);
                }
                else
                {
                    compiledScripts = existing;
                }
            }
            else
            {
                Compile();
            }



            LoadScriptAssembly();

        }

        private static string CreateTextualHash(byte[] hash)
        {
            StringBuilder textHash = new StringBuilder();
            foreach (byte b in hash)
            {
                textHash.Append(b.ToString());
            }
            return textHash.ToString();
        }

        private void Compile()
        {
            Compile(HashSource());
        }

        private byte[] HashSource()
        {
            List<byte> bytes = new List<byte>();
            foreach (string s in source)
            {
                foreach (char c in s)
                {
                    bytes.Add(Convert.ToByte(c));
                }
            }
            return bytes.ToArray();
        }

        private void Compile(byte[] hash)
        {
            compilerResults = RecompileSource(hash);

            if (compilerResults.Errors.Count == 0)
            {
                System.Diagnostics.Trace.WriteLine(compilerResults.PathToAssembly, "Scripts compiled");
                compiledScripts = compilerResults.CompiledAssembly;
            }
            else
            {
                foreach (CompilerError error in compilerResults.Errors)
                {
                    System.Diagnostics.Trace.WriteLine(error.ErrorText, "Script compile error");
                }
                System.Diagnostics.Debug.Fail("Errors compiling scripts.", compilerResults.Errors.ToString());
                throw new ScriptCompileException("Errors compiling scripts.", compilerResults);
            }
        }

        private void LoadScriptAssembly()
        {
            ////set up a seperate, sandboxed domain for the scripts to run in, with reduced privilidges.
            //AppDomainSetup setupInfo = new AppDomainSetup();

            //System.Diagnostics.Trace.WriteLine(setupInfo.PrivateBinPathProbe, "Probe Directory");
            //scriptDomain = AppDomain.CreateDomain("Scripts Domain", compiledScripts.Evidence, setupInfo);

            //scriptDomain.Load(compiledScripts.GetName());

            //scriptDomain.SetAppDomainPolicy(this.securityLevel);
        }

        private CompilerResults RecompileSource(byte[] hash)
        {
            //set up compilation parameters
            CompilerParameters paramaters = new CompilerParameters();
            paramaters.GenerateExecutable = false;
            paramaters.GenerateInMemory = false;

            //reference every loaded assembly
            Assembly[] loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
            string[] referenceAssemblies = new string[loadedAssemblies.Length];
            for (int i = 0; i < referenceAssemblies.Length; i++)
            {
                referenceAssemblies[i] = loadedAssemblies[i].Location;
            }


            MD5 sum = MD5.Create();
            Array.Resize<string>(ref source, source.Length + 1);
            string assemblyAttributes = "[assembly:Xfinity.Scripting.Hash(\"" + CreateTextualHash(sum.ComputeHash(hash)) + "\")]\n";
            assemblyAttributes = assemblyAttributes + "[assembly:Xfinity.Scripting.ClrVersion(\"" + Environment.Version.ToString() + "\")]\n";
            assemblyAttributes = assemblyAttributes + "[assembly: System.Security.Permissions.ZoneIdentityPermission(System.Security.Permissions.SecurityAction.RequestMinimum,Zone= System.Security.SecurityZone.MyComputer,Unrestricted=true)]\n";

            source[source.Length - 1] = assemblyAttributes;
            //add the references to the compiler
            paramaters.ReferencedAssemblies.AddRange(referenceAssemblies);
            paramaters.Evidence = AppDomain.CurrentDomain.Evidence;

            //misc. options
            paramaters.OutputAssembly = Xfinity.Resources.Resources.CompiledScriptsPath;
            Directory.CreateDirectory(Path.GetDirectoryName(Xfinity.Resources.Resources.CompiledScriptsPath));
            paramaters.IncludeDebugInformation = true;
            paramaters.CompilerOptions = "/optimize";
            return provider.CompileAssemblyFromSource(paramaters, source);
        }

        AppDomain scriptDomain;
        string[] source;
        CodeDomProvider provider;
        private static Assembly compiledScripts;

        /// <summary>
        /// Gets the compiled scripts.
        /// </summary>
        /// <value>The compiled scripts.</value>
        public static Assembly CompiledScripts
        {
            [DebuggerStepThrough]
            get { return compiledScripts; }
        }

        private CompilerResults compilerResults;

        /// <summary>
        /// Gets the compiler results.
        /// </summary>
        /// <value>The compiler results.</value>
        public CompilerResults CompilerResults
        {
            [DebuggerStepThrough]
            get { return compilerResults; }
        }
    }
}